﻿//-----------------------------------------------------------------------
// <copyright file="ProcessWrapper.cs" author="Artak Mkrtchyan">
//     Creative Commons - GNU General Public License (http://creativecommons.org/licenses/GPL/2.0/
// </copyright>
// <author>Artak Mkrtchyan</author>
// <email>mkArtak@gmail.com</email>
// <date>15/07/2012</date>
//-----------------------------------------------------------------------
using ConsoleHoster.Common.Utilities;
using System;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;

namespace ConsoleHoster.Model
{
	public delegate void ProcessWrapperEventHandler(ProcessWrapper argSender);
	public delegate void ProcessWrapperMessageEventHandler(ProcessWrapper argSender, ProcessMessage argMessage);

	public class ProcessWrapper : IDisposable
	{
		public event ProcessWrapperEventHandler ProcessExited;
		public event ProcessWrapperMessageEventHandler DataReceived;

		private Process process;
		private readonly ProcessStartInfo startInfo;

		private bool isDisposed = false;
		private readonly Object disposingSyncer = new Object();

		private StreamObserver outputStreamObserver;
		private StreamObserver errorStreamObserver;
		private Thread messageNotifierThread;

		private readonly ILogger logger;

		public ProcessWrapper(ProcessStartInfo argInfo, ILogger argLogger)
		{
			ValidationManager.RequireArgumentNotNull(argInfo, "argInfo");

			this.logger = new ComponentLogger("ProcessWrapper", argLogger);

			this.startInfo = new ProcessStartInfo();
			this.startInfo.RedirectStandardError = true;
			this.startInfo.RedirectStandardInput = true;
			this.startInfo.RedirectStandardOutput = true;
			this.startInfo.UseShellExecute = false;
			this.startInfo.CreateNoWindow = true;
			this.startInfo.Arguments = argInfo.Arguments;
			this.startInfo.FileName = argInfo.FileName;
			this.startInfo.WorkingDirectory = argInfo.WorkingDirectory;
		}

		public void StartProcess()
		{
			if (this.process != null)
			{
				throw new Exception("Process already started");
			}

			try
			{
				this.process = this.CreateUnderlyingProcess(true);

				this.process.PriorityClass = ProcessPriorityClass.BelowNormal;
				this.outputStreamObserver = this.BeginReadStream(this.process.StandardOutput, "Unable to read output stream");
				this.errorStreamObserver = this.BeginReadStream(this.process.StandardError, "Unable to read error stream");

				this.StartMessageNotifier();
			}
			catch (Exception ex)
			{
				this.logger.LogError("Unable to start the underying process. Details: {0}", ex.ToString());
				if (this.process != null)
				{
					this.process.Exited -= OnProcess_Exited;
					this.process.Dispose();
					this.process = null;
				}

				throw;
			}
		}

		private void StartMessageNotifier()
		{
			this.messageNotifierThread = new Thread(new ThreadStart(ProcessIncomingMessages));
			this.messageNotifierThread.IsBackground = true;
			this.messageNotifierThread.Priority = ThreadPriority.Lowest;
			this.messageNotifierThread.Start();
		}

		private void ProcessIncomingMessages()
		{
			StreamMessage? tmpErrorMessage = null;
			StreamMessage? tmpOutputMessage = null;

			while (!this.isDisposed)
			{
				if (!tmpErrorMessage.HasValue)
				{
					tmpErrorMessage = this.errorStreamObserver.GetNextAvailableMessage();
				}

				if (!tmpOutputMessage.HasValue)
				{
					tmpOutputMessage = this.outputStreamObserver.GetNextAvailableMessage();
				}

				bool? tmpSelectedMessage = this.NotifyMessage(tmpOutputMessage, tmpErrorMessage);
				if (tmpSelectedMessage.HasValue)
				{
					if (tmpSelectedMessage.Value)
					{
						tmpOutputMessage = null;
					}
					else
					{
						tmpErrorMessage = null;
					}
				}
				else
				{
					Thread.Sleep(50);
				}
			}
		}

		private Process CreateUnderlyingProcess(bool argSupportProcessGroups)
		{
			Process tmpProcess = new Process();
			tmpProcess.StartInfo = this.startInfo;
			tmpProcess.Exited += OnProcess_Exited;
			tmpProcess.EnableRaisingEvents = true;

			tmpProcess.Start();
			return tmpProcess;
		}

		private StreamObserver BeginReadStream(StreamReader argStream, string argErrorMessage)
		{
			StreamObserver tmpResult;
			try
			{
				tmpResult = new StreamObserver(argStream, this.logger);
				tmpResult.BeginObservation();
			}
			catch (Exception ex)
			{
				this.logger.LogError("Error while reading stream. Details: {0}", ex.ToString());
				throw new Exception(argErrorMessage, ex);
			}

			return tmpResult;
		}

		private bool? NotifyMessage(StreamMessage? argOutputMessage, StreamMessage? argErrorMessage)
		{
			bool? tmpResult = null;

			if (argOutputMessage.HasValue && argErrorMessage.HasValue)
			{
				tmpResult = argOutputMessage.Value.DateReceived <= argErrorMessage.Value.DateReceived;
			}
			else if (argOutputMessage.HasValue)
			{
				tmpResult = true;
			}
			else if (argErrorMessage.HasValue)
			{
				tmpResult = false;
			}

			if (tmpResult.HasValue)
			{
				this.OnMessageReceived(new ProcessMessage(tmpResult.Value ? argOutputMessage.Value : argErrorMessage.Value, tmpResult.Value));
			}

			return tmpResult;
		}

		private void OnMessageReceived(ProcessMessage argMessage)
		{
			ProcessWrapperMessageEventHandler temp = this.DataReceived;
			if (temp != null)
			{
				temp(this, argMessage);
			}
		}

		private void OnProcess_Exited(object sender, EventArgs e)
		{
			ProcessWrapperEventHandler temp = this.ProcessExited;
			if (temp != null)
			{
				temp(this);
			}
		}

		public void SendData(string argCommand)
		{
			this.process.StandardInput.WriteLine(argCommand);
		}

		public void Dispose()
		{
			this.Dispose(true);
		}

		private void Dispose(bool argDisposing)
		{
			if (!this.isDisposed)
			{
				new Action(() =>
				{
					if (!this.isDisposed)
					{
						if (argDisposing && this.process != null)
						{
							this.process.Exited -= OnProcess_Exited;
							this.TryBreak();

							this.SafelyDisposeObserver(this.outputStreamObserver);
							this.SafelyDisposeObserver(this.errorStreamObserver);

							try
							{
								this.process.Kill();
								this.process.WaitForExit();
							}
							catch
							{
							}
							finally
							{
								if (this.process != null)
								{
									this.process.Close();
									this.process.Dispose();
									this.process = null;
								}
							}
						}

						this.isDisposed = true;
					}
				}).SyncronizeCallByLocking(this.disposingSyncer);
			}
		}

		public void TryBreak()
		{
			this.SendData(Char.ConvertFromUtf32(3));
		}

		[MethodImpl(MethodImplOptions.NoInlining)]
		private void SafelyDisposeObserver(StreamObserver argObserver)
		{
			if (argObserver != null)
			{
				argObserver.Dispose();
			}
		}

		public string Name
		{
			get
			{
				string tmpResult = null;
				if (this.process != null)
				{
					tmpResult = this.process.ProcessName;
				}
				return tmpResult;
			}
		}
	}

	public enum ProcessMessageTypes
	{
		Output,
		Error
	}
}